/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package ua.yandex.collections;

/**
 *
 * @author vindex10
 */
public final class MyLinkedList implements MyList {
  static final int MAXLENGTH = Integer.MAX_VALUE;
  private Node head;
  private int length;
  
  public MyLinkedList() {
    length = 0;
  }
  public MyLinkedList(final Object ... in) {

//    uncomment to check for overflow manually
//    myLinkedListPrecheck(in);
    
    length = 0;
    addAll(in);
  }
  
//  private void myLinkedListPrecheck(Object ... in) {
//    if (MAXLENGTH < in.length) {
//      throw new IndexOutOfBoundsException();
//    }
//  }
  
  @Override
  public void add(final Object o) {
    if (o == null)
    {
      return;
    }
    if (isEmpty()) {
      head = new Node(null, null, o);
      length++;
    } else {
      Node last = getNode(length - 1);
      addAfterNode(last, o);
    }
  }
  
  @Override
  public void add(final int index, final Object o) {
    addPrecheck(index, o);
    if (o == null)
    {
      return;
    }
    if (isNextAfterLast(index)) {
      add(o);
    } else {
      addBeforeNode(getNode(index), o);
    }
  }
  private void addPrecheck(int index, Object o) {
    if (!inList(index) && !isNextAfterLast(index)) {
      throw new ArrayIndexOutOfBoundsException();
    }
  }
  
  @Override
  public void addAll(final Object[] o) {
    
//    uncomment to check for overflow manually
//    addAllPrecheck(o);
    
    if (o == null || o.length == 0)
    {
      return;
    }
    Node cur;
    if (isEmpty()) {
      cur = new Node(null, null, o[0]);
      head = cur;
      length++;
    } else {
      cur = getNode(getLength() - 1);
      cur = addAfterNode(cur, o[0]);
    }
    for (int i = 1; i < o.length; i++) {
        cur = addAfterNode(cur, o[i]);
    }
  }
  
//  private void addAllPrecheck(Object[] o) {
//    if (MAXLENGTH - o.length < length) {
//      throw new IndexOutOfBoundsException();
//    }
//  }
  
  @Override
  public void addAll(int index, final Object[] o) {
    addAllPrecheck(index, o);
    if (o == null || o.length == 0)
    {
      return;
    }
    if (isNextAfterLast(index)) {
      addAll(o);
    } else {
      Node cur = getNode(index); //throws exception if index is wrong
      for (Object elem: o) {
        addBeforeNode(cur, elem);
      }
    }
  }
  private void addAllPrecheck(int index, Object[] o) {
    if (!inList(index) && !isNextAfterLast(index)) {
      throw new ArrayIndexOutOfBoundsException();
    }
    
//    uncomment to check for overflow manually
//    if (MAXLENGTH - o.length < length) {
//      throw new IndexOutOfBoundsException();
//    }
    
  }
  
  @Override
  public Object get(int index) {
    return getNode(index).getValue();
  }
  
  @Override
  public void set(int index, Object o) {
    getNode(index).setValue(o);
  }
  
  @Override
  public Object remove(int index) {
    Node res = getNode(index);
    if (res.hasPrev()) {
      res.getPrev().setNext(res.getNext());
    }
    if (res.hasNext()) {
      res.getNext().setPrev(res.getPrev());
    }
    length--;
    if (index == 0) {
      head = res.getNext();
    }
    return res.getValue();
  }
  
  @Override
  public int indexOf(Object o) {
    Node cur = head;
    int i = 0;
    while (i < getLength() && !cur.getValue().equals(o)) {
      cur = cur.getNext();
      i++;
    }
    if (i == getLength()) {
      return -1;
    } else {
      return i;
    }
  }
  
  @Override
  public int size() {
    return getLength();
  }
  
  @Override
  public void clear() {
    head = null;
    length = 0;
  }
  
  @Override
  public boolean isEmpty() {
    return length == 0;
  }
  
  @Override
  public Object[] toArray() {
    Object[] res = new Object[getLength()];
    if (getLength() == 0) {
      return res;
    }
    Node cur = head;
    int i = 0;
    while (cur.hasNext()) {
      res[i] = cur.getValue();
      i++;
      cur = cur.getNext();
    }
    res[i] = cur.getValue();
    return res;
  }
  
  @Override
  public String toString() {
    StringBuilder res = new StringBuilder("");
    if (isEmpty()) {
      return res.toString();
    }
    Node cur = head;
    while (cur.hasNext()) {
      res.append(cur.getValue().toString()).append(", ");
      cur = cur.getNext();
    }
    res.append(cur.getValue().toString());
    return res.toString();
  }
  
  public void addFirst(Object e) {
    add(0, e);
  }
  
  public void addLast(Object e) {
    add(e);
  }
  
  public Object getFirst() {
    return get(0);
  }
  
  public Object getLast() {
    return get(getLength() - 1);
  }
  
  public Object removeFirst() {
    return remove(0);
  }
  
  public Object removeLast() {
    return remove(getLength() - 1);
  }
  
  //Serving methods
  private Node getNode(int index) {
    getNodePrecheck(index);
    Node cur = head;
    for (int i = 0; i < index; i++) {
      cur = cur.getNext();
    }
    return cur;
  }
  private void getNodePrecheck(int index) {
    if (getLength() - 1 < index || index < 0) {
      throw new ArrayIndexOutOfBoundsException();
    }
  }
  
  private Node addBeforeNode(Node index, Object o) {

//    You can uncomment it, to check for overflow manually
//    addBeforeNodePrecheck();
    
    Node newNode = new Node(index.getPrev(), index, o);
    if (index.hasPrev()) {
      index.getPrev().setNext(newNode);
    } else {
      head = newNode;
    }
    index.setPrev(newNode);
    length++;
    return newNode;
  }
  
//  private void addBeforeNodePrecheck() {
//    if (MAXLENGTH - 1 < getLength()) {
//      throw new IndexOutOfBoundsException();
//    }
//  }
  
  private Node addAfterNode(Node index, Object o) {

//    You can uncomment it to check for overflow manually.
//    addAfterNodePrecheck(); 

    Node newNode = new Node(index, index.getNext(), o);

//  That part is needed for consistency, but i don't use it in project.
//    if (index.hasNext()) {
//      index.getNext().setPrev(newNode);
//    }

    index.setNext(newNode);
    length++;
    return newNode;
  }

//  private void addAfterNodePrecheck() {
//    if (MAXLENGTH - 1 < getLength()) {
//      throw new IndexOutOfBoundsException();
//    }
//  }
  
  //Small scripts
  
  private int getLength() {
    return length;
  }
  private boolean inList(int index) {
    if (index >= 0 && index < getLength()) {
      return true;
    }
    
    return false;
  }
  private boolean isNextAfterLast(int index) {
    if (index == getLength()) {
      return true;
    }
    
    return false;
  }
  
  // Node class
  static class Node {
    private Node prev;
    private Node next;
    
    private Object value;
    
    public Node(Object o) {
      value = o;
      prev = null;
      next = null;
    }
    public Node(Node prev, Node next, Object o) {
      this(o);
      this.prev = prev;
      this.next = next;
    }
    private void setNext(Node o) {
      next = o;
    }
    private Node getNext() {
      return next;
    }
    private Node getPrev() {
      return prev;
    }
    private void setPrev(Node o) {
      prev = o;
    }
    public boolean hasPrev() {
      return prev != null;
    }
    public boolean hasNext() {
      return next != null;
    }
    public Object getValue() {
      return value;
    }
    private void setValue(Object o) {
      value = o;
    }
  }
  
}
